iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 23
1

VAPID Library是使用「非對稱式加密的方式」確保傳送推播訊息到瀏覽器供應商的push server是「來自我自己的server」。

也就是說它會產生一組公私鑰對,私鑰儲存在我自己的server。公鑰則是當已訂閱的用戶發佈新貼文時,連同「API endpoint」和「一些金鑰交換的資訊」一起傳送出去(這段後面會說明),push server就會根據這些資訊來驗證是否這是從我的server發送的推播訊息。

首先,要先在我的backend server(也就是Firebase Cloud Functions)中新增VAPID package:

npm install --save web-push

接著在package.json裡添加執行這個modules的scripts:

"scripts": {
    "web-push": "web-push"
}

之後在terminal中我就可以透過執行npm run web-push generate-vapid-keys來產生一組公私鑰對:

BTW 請好好管理自己的私鑰,千萬不要外流

接著在app.js的configurePushSub()繼續完成昨天還沒完成的設定:

function configurePushSub() {
    if(!('serviceWorker' in navigator)) {
        return;
    }
    var reg;
    navigator.serviceWorker.ready.then(function(swreg) {
        reg = swreg;
        return swreg.pushManager.getSubscription();
    }).then(function(sub) {
        if(sub === null) {
            // Create a new subscription
            var vapidPublicKey = 'BDIOql6aKK-00AGzVKggeN9LSpjGd2golLzuiCvmUG0NAIa3wi-FmG17HElLHhXtzQBQQ9faZmJ2MWW87VI8bgg';
            var convertedVapidPublicKey = urlBase64ToUint8Array(vapidPublicKey);
            return reg.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: convertedVapidPublicKey
            });
        } else {
            // We have a subscription
        }
    }).then(function(newSub) {
    return fetch('https://trip-diary-f56de.firebaseio.com/subscriptions.json', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify(newSub)
        })
    }).then(function(res) {
        if(res.ok) {
            displayConfirmNotification();
        }
    }).catch(function(err) {
        console.log(err);
    })
}

在用戶按下訂閱鈕後,添加剛剛產生的public key到pushManager.subscribe()中。不過這裡要先進行編碼的轉換,因為產生的公鑰是base64編碼,但subscribe()接受的是今鑰編碼是一個Uint8陣列,這裡直接使用gist上已經寫好的function進行轉換(我把這個function加到utility.js中)。

瀏覽器供應商push server接收到這個訂閱訊息後,會回傳剛剛所說的「API Endpoint」+「金鑰交換」資訊,我將這些資訊儲存在後台firebase資料庫中,最後我才將顯示「成功訂閱」的通知。

firebase儲存的訂閱資訊:


再來回到Firebase Cloud Funcitons的StorePostData() API。當用戶發佈新貼文時,會呼叫這個API並儲存到後台資料庫中,不過現在除了儲存這些貼文資訊,我現在還要將push message傳送到前面從push server取得的API Endpoint

來看一下該怎麼做:

var webpush = require('web-push');

exports.storePostData = functions.https.onRequest((request, response) => {
    cors(request, response, function() {
        admin.database().ref('posts').push({
            id: request.body.id,
            title: request.body.title,
            location: request.body.location,
            image: request.body.image
        }).then(function() {
            webpush.setVapidDetails('mailto:j84077200345@gmail.com', 'BDIOql6aKK-00AGzVKggeN9LSpjGd2golLzuiCvmUG0NAIa3wi-FmG17HElLHhXtzQBQQ9faZmJ2MWW87VI8bgg', 'JxB633wEwprQT3hahwrNPoimHshPRj0Kd9OK11IXlQ8');
            return admin.database().ref('subscriptions').once('value');
        }).then(function(subscriptions) {
            subscriptions.forEach(function(sub) {
                var pushConfig = {
                    endpoint: sub.val().endpoint,
                    keys: {
                        auth: sub.val().keys.auth,
                        p256dh: sub.val().keys.p256dh
                    }
                };

                webpush.sendNotification(pushConfig, JSON.stringify({title: '新貼文', content: '有新增的貼文!!'})).catch(function(err) {
                    console.log(err);
                });
            });
            response.status(201).json({message: 'Data Stored', id: request.body.id});
        }).catch(function(err) {
            response.status(500).json({error: err});
        })
    });
});

首先一樣要導入web-push套件,使用webpush.setVapidDetails()來設定server的金鑰資訊(第一個參數為server的對外信箱,第二個為public key,最後是private key)。

設定完成後,要先從資料庫裡獲取目前所有訂閱用戶的資訊,這樣才可以針對每個有訂閱的用戶來發送推播通知。所以這裡使用webpush.sendNotification() method將「資料庫中的每一筆用戶訂閱資訊(endpoint+keys)」和「要推送的訊息(json format)」push到瀏覽器供應商的push server。


目前我們的server已經可以push messages到瀏覽器供應商的push server了。現在準備要在service worker監聽這個「push事件」,當監聽到這個push event,service worker就可以將有新貼文的訊息通知給所有訂閱用戶,就算今天用戶沒有開啟PWA。

在sw.js中監聽push事件:

self.addEventListener('push', function(event) {
    console.log('Push Notification', event);
    var data = {title: 'New!', content: 'Something new!'};

    if(event.data) {
        data = JSON.parse(event.data.text());
    }

    var options = {
        body: data.content,
        icon: '/src/images/icons/app-icon-96x96.png',
        badge: '/src/images/icons/app-icon-96x96.png'
    };

    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

這裡監聽到的事件,裡面的data屬性就是我剛剛在server中在sendNotification()輸入的的json資料(也就是{title: '新貼文', content: '有新增的貼文!!'})。

接著跟前面實作「顯示通知」時一樣,會設定一些選項(body、icon、badge...)。最後必須要取得正在browser運行的已註冊service worker(self.registration),並呼叫showNotification()將要顯示的選項設定和內容傳入。

看一下當我新增一篇貼文時,顯示通知的結果:


最後我要繼續完成之前在實作「顯示通知」時,用戶點擊通知時互動的部分。回到service worker中監聽notificationclick事件,現在要實作當用戶點擊通知時,會導向或開啟我的PWA頁面。

首先使用clients.match()取得所有開啟的視窗或頁面,它會回傳一個promise。

根據回傳的陣列(我把它命名為clis)使用js的find函式確認是否有由這個service worker管理的視窗(也就是我的PWA頁面是否開啟的)。有的話(不等於undefinded)就navigate到這段url(notification.data.url),沒有則open一個新window開啟我們的PWA。

self.addEventListener('notificationclick', function(event) {
    var notification = event.notification;
    var action = event.action;

    console.log(notification);
    
    if(action === 'confirm') {
        console.log('Confirm was chosen');
        notification.close();
    } else {
        console.log(action);
        event.waitUntil(
            clients.matchAll().then(function(clis) {
                var client = clis.find(function(c) {
                    return c.visibilityState === 'visible';
                });

                if(client !== undefined) {
                    client.navigate(notification.data.url);
                    client.focus();
                } else {
                    clients.openWindow(notification.data.url);
                }
                notification.close();
            })
        );
    }
});

說明一下notification.data.url是怎麼來的?

我後來在server傳遞的push messages中又加入了openUrl{title: '新貼文', content: '有新增的貼文!!', openUrl: '/'},代表著用戶點擊通知時要導向的頁面。

這樣service worker監聽到這個push event時,就可以在選項中將這個url設定到data這個屬性。

var options = {
        body: data.content,
        icon: '/src/images/icons/app-icon-96x96.png',
        badge: '/src/images/icons/app-icon-96x96.png',
        data: {
            url: data.openUrl
        }
};

最後當**用戶點擊事件(notificationclick)**發生時,service worker就可以從notification.data.url知道要導向的url是什麼。

推播通知的實作就到這啦!!

Day24 結束!! /images/emoticon/emoticon72.gif


上一篇
[Day23] 實作PWA推播通知(Part2)
下一篇
[Day25] 了解Media API和Geolocation API(Part1)
系列文
你應該要知道的新一代Web技術---漸進式網頁(PWA)29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言